package parsetree;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Throwables.propagate;
import static util.ArrayConcat.concatenate;

import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.event.DocumentEvent.ElementChange;
import javax.swing.text.AbstractDocument.ElementEdit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import metagrammar.Type;
import parsetree.exceptions.DocumentContractBrokenException;
import util.TrieMap;
import util.TrieSet;

public class MultiPartParsingElement extends AbstractBranchElement {
    
    public static final String NAME = "multi-part_parsing";
    
    private static final AttributeSet DEFAULT_STYLE;
    static {
        SimpleAttributeSet attributes = new SimpleAttributeSet();
        attributes.addAttribute(StyleConstants.Foreground, Color.GRAY);
        DEFAULT_STYLE = attributes;
    }
    
    private final TrieSet<ElementFactory>.TrieSearchResult suffixMap;
    
    public MultiPartParsingElement(BranchElement parent, int startOffset, int prefixLength,
            int endOffset, TrieMap<ElementFactory>.ImmutableTrieMap suffixMap)
                    throws BadLocationException
                    {
        super(parent, new ArrayList<Element>());
        addChild(new OperatorLiteral(startOffset, startOffset + prefixLength));
        addChild(new Lexer(startOffset + prefixLength, endOffset));
        this.suffixMap = suffixMap;
                    }
    
    private MultiPartParsingElement(BranchElement parent, MultiPartParsingElement oldElement,
            int newEndOffset) throws BadLocationException
            {
        super(parent, oldElement.getChildren());
        checkArgument(newEndOffset > getEndOffset());
        this.suffixMap = oldElement.suffixMap;
        addChild(new Lexer(getEndOffset(), newEndOffset));
            }
    
    private MultiPartParsingElement(BranchElement parent, int splitIndex,
            MultiPartParsingElement oldElement)
    {
        super(parent, oldElement.getChildren().subList(0, splitIndex));
        this.suffixMap = oldElement.suffixMap;
    }
    
    @Override
    public Type getType()
    {
        return Type.UNTYPED;
    }
    
    @Override
    public String getName()
    {
        return NAME;
    }
    
    @Override
    public boolean isLeaf()
    {
        return false;
    }
    
    @Override
    public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
    {
        int offset = change.getOffset();
        if(offset < getStartOffset() || offset > getEndOffset()) {
            return null;
        }
        
        List<ElementChange> changes = new LinkedList<ElementChange>();
        
        boolean updated = false;
        Element toChange = getChildContaining(offset);
        if(toChange != null) {
            if( !(toChange instanceof OperatorLiteral)) {
                List<ElementChange> elemChange = toChange.insertUpdate(change, attributes);
                if(elemChange != null) {
                    changes.addAll(elemChange);
                }
            }
            else {
                try {
                    ElementEdit parentChange = getParent().addChildren(split(offset), true, true);
                    changes.add(parentChange);
                    return changes;
                }
                catch(BadLocationException e) {
                    propagate(e);
                }
            }
        }
        else {
            int index = getIndexOfChildEndsWith(offset);
            if(index >= 0) {
                toChange = getElement(index);
                if( !(toChange instanceof OperatorLiteral)) {
                    List<ElementChange> elemChange = toChange.insertUpdate(change, attributes);
                    if(elemChange != null) {
                        changes.addAll(elemChange);
                    }
                }
                else {
                    if(index == getChildCount() - 1) {
                        try {
                            changes.add(addChild(new Lexer(offset, offset + change.getLength())));
                        }
                        catch(BadLocationException e) {
                            throw new DocumentContractBrokenException(e);
                        }
                    }
                }
            }
            toChange = getChildStartsWith(offset);
            if(toChange != null && !(toChange instanceof OperatorLiteral)) {
                List<ElementChange> elemChange = toChange.insertUpdate(change, attributes);
                if(elemChange != null) {
                    changes.addAll(elemChange);
                }
            }
        }
        if( !updated) {
            Lexer newLeaf;
            try {
                newLeaf = new Lexer(offset, offset + change.getLength());
                changes.add(addChild(newLeaf));
                List<ElementChange> elemChange = newLeaf.insertUpdate(change, attributes);
                if(elemChange != null) {
                    changes.addAll(elemChange);
                }
            }
            catch(BadLocationException e) {
                propagate(e);
            }
        }
        return changes;
    }
    
    @Override
    public List<ElementChange> postRemoveUpdate(Removal change)
    {
        int offset = change.getOffset();
        if(offset < getStartOffset() || offset > getEndOffset()) {
            return null;
        }
        
        LinkedList<ElementChange> changes = new LinkedList<ElementChange>();
        
        boolean updated = false;
        Element toChange = getChildContaining(offset);
        if(toChange != null) {
            if(toChange instanceof OperatorLiteral) {
                ParsingElement replacement;
                try {
                    replacement = new ParsingElement(getParent(), getStartOffset(), getEndOffset());
                }
                catch(BadLocationException e) {
                    throw propagate(e);
                }
                ElementEdit parentChange = getParent().addChild(replacement);
                changes.add(parentChange);
            }
            else {
                List<ElementChange> childChanges = toChange.postRemoveUpdate(change);
                if(childChanges != null) {
                    changes.addAll(childChanges);
                }
            }
        }
        else {
            toChange = getChildStartsWith(offset);
            if(toChange != null && !(toChange instanceof OperatorLiteral)) {
                List<ElementChange> childChanges = toChange.postRemoveUpdate(change);
                if(childChanges != null) {
                    changes.addAll(childChanges);
                }
            }
            toChange = getChildEndsWith(offset);
            if(toChange != null && !(toChange instanceof OperatorLiteral)) {
                List<ElementChange> childChanges = toChange.postRemoveUpdate(change);
                if(childChanges != null) {
                    changes.addAll(childChanges);
                }
            }
        }
        if( !updated) {
            MultiPartParsingElement element = new MultiPartParsingElement(getParent(), offset, this);
            getParent().addChild(element);
        }
        return changes;
    }
    
    @Override
    public Element merge(Element element)
    {
        if(getParent().equals(element.getParent())) {
            if(element instanceof MultiPartParsingElement) {
                if(getEndOffset() == element.getStartOffset()) {
                    return handleMerge(this, (MultiPartParsingElement) element);
                }
                else if(getStartOffset() == element.getEndOffset()) {
                    return handleMerge((MultiPartParsingElement) element, this);
                }
            }
            else if(element instanceof ParsingElement) {
                //TODO
            }
        }
        return null;
    }
    
    /**
     * Handles the actual merge between two adjacent parse elements.
     * @param firstElement
     * @param element
     * @return
     */
    private Element handleMerge(MultiPartParsingElement firstElement, MultiPartParsingElement secondElement)
    {
        BranchElement parent = firstElement.getParent();
        
        try {
            return new MultiPartParsingElement(parent, this, secondElement.getEndOffset());
        }
        catch(BadLocationException e) {
            throw propagate(e);
        }
    }
    
    
    @Override
    public Element[] split(int modelOffset) throws BadLocationException
    {
        checkArgument(modelOffset > getStartOffset() && modelOffset < getEndOffset());
        int splitIndex = getIndexOfChildContaining(modelOffset);
        if(splitIndex >= 0) {
            // a child is split
            if(splitIndex == 0) {
                // this Element is no longer valid
                Element splitChild = getElement(0);
                int childEnd = splitChild.getEndOffset();
                Element[] splitChildren = splitChild.split(modelOffset);
                ParsingElement remainder = new ParsingElement(getParent(), childEnd, getEndOffset());
                return concatenate(splitChildren, new Element[] { remainder });
            }
            else {
                MultiPartParsingElement mppElement = new MultiPartParsingElement(getParent(), splitIndex, this);
                Element splitChild = getElement(splitIndex);
                Element[] split;
                int childEnd = splitChild.getEndOffset();
                ParsingElement remainder = null;
                if(splitChild instanceof Lexer) {
                    split = splitChild.split(modelOffset);
                    mppElement.addChild(split[0]);
                    if(splitIndex < getChildCount()) {
                        remainder = new ParsingElement(getParent(), childEnd,
                                getEndOffset());
                        
                    }
                    split = new Element[] { mppElement, split[1], remainder };
                }
                else {
                    split = splitChild.split(modelOffset);
                    if(splitIndex < getChildCount()) {
                        remainder = new ParsingElement(getParent(), childEnd, getEndOffset());
                        split = concatenate(new Element[] { mppElement }, split,
                                new Element[] { remainder });
                    }
                }
                return split;
            }
        }
        else {
            // split between children
            int correctedIndex = ( -splitIndex) - 1;
            Element mppElement = new MultiPartParsingElement(getParent(), correctedIndex, this);
            ParsingElement remainder = new ParsingElement(getParent(), correctedIndex,
                    getEndOffset());
            return new Element[] { mppElement, remainder };
        }
    }
    
    
    private class Lexer extends AbstractLeafElement implements Element {
        
        public static final String NAME = "parser";
        private final Position endPosition;
        
        public Lexer(int startOffset, int endOffset)
                throws BadLocationException
                {
            super(MultiPartParsingElement.this, startOffset);
            endPosition = getDocument().createPosition(endOffset);
                }
        
        @Override
        public String getName()
        {
            return NAME;
        }
        
        @Override
        public Type getType()
        {
            return Type.UNTYPED;
        }
        
        private List<ElementChange> parseString(Insert change)
        {
            LinkedList<TrieMap<ElementFactory>.ImmutableTrieMap> previousMaps = new LinkedList<TrieMap<ElementFactory>.ImmutableTrieMap>();
            TrieMap<ElementFactory>.ImmutableTrieMap currentMap = suffixMap;
            int length = getStartOffset();
            StyledDocument document = getDocument();
            LinkedList<ElementChange> elementChanges = new LinkedList<ElementChange>();
            try {
                for(; currentMap != null && length < getEndOffset(); length++) {
                    previousMaps.push(currentMap);
                    currentMap = currentMap.getSubMap(document.getText(length, 1));
                }
                if(currentMap == null) {
                    // if there is only one possible parsing solution before the end of the text is reached
                    TrieMap<ElementFactory>.ImmutableTrieMap map;
                    do { // find the last valid parse
                        map = previousMaps.pop();
                    } while( !previousMaps.isEmpty() && map.getValueAtSuffix("") == null);
                    if( !previousMaps.isEmpty() || map.getValueAtSuffix("") != null) {
                        // if a valid parse is found
                        // recover the operator
                        ElementFactory spec = map.getValueAtSuffix("");
                        // create a new operator element representing the operator
                        int start = getStartOffset();
                        int operatorLength = map.getCurrentKey().length();
                        BranchElement parent = getParent();
                        Element element = spec.generateElement(parent, start, start
                                + operatorLength);
                        elementChanges.add(parent.addChild(element));
                    }
                    else {
                        elementChanges
                        .add(new ElementEdit(this, this.getStartOffset(), null, null));
                    }
                }
                else if( !currentMap.hasSubMap()) {
                    // the end of the text has been reached.
                    // if there is only one possible parsing solution...
                    TrieMap<ElementFactory>.ImmutableTrieMap map = currentMap;
                    while( !previousMaps.isEmpty() && map.getValueAtSuffix("") == null) {
                        map = previousMaps.pop();
                    } // find the last valid solution
                    if( !previousMaps.isEmpty() || map.getValueAtSuffix("") != null) {
                        ElementFactory operatorSpec = map.getValueAtSuffix("");
                        BranchElement parent = getParent();
                        Element element = operatorSpec.generateElement(parent,
                                getStartOffset(), getEndOffset());
                        elementChanges.add(parent.addChild(element));
                    }
                    else {
                        elementChanges
                        .add(new ElementEdit(this, this.getStartOffset(), null, null));
                    }
                }
            }
            catch(BadLocationException e1) {
                throw new DocumentContractBrokenException("Parsing has become impossible.");
            }
            return elementChanges;
        }
        
        @Override
        public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
        {
            if(change.getOffset() < getStartOffset()
                    && change.getOffset() + change.getLength() <= getStartOffset()) {
                return null;
            }
            return parseString(change);
        }
        
        @Override
        public List<ElementChange> postRemoveUpdate(Removal change)
        {
            if(getStartOffset() == getEndOffset()) {
                ElementEdit removal = getParent().removeEmptyChild(this);
                LinkedList<ElementChange> changes = new LinkedList<ElementChange>();
                changes.add(removal);
                return changes;
            }
            return null;
        }
        
        @Override
        public Element merge(Element element)
        {
            // TODO Auto-generated method stub
            return null;
        }
        
        @Override
        public Element[] split(int modelOffset) throws BadLocationException
        {
            checkArgument(modelOffset > getStartOffset() && modelOffset < getEndOffset());
            // TODO
            return new Element[] { new Lexer(getStartOffset(), modelOffset),
                    getElementAfter(modelOffset) };
        }
        
        @Override
        public int getEndOffset()
        {
            return endPosition.getOffset();
        }
        
        @Override
        public void addAttributes(AttributeSet newAttributes)
        {
        }
    }
    
    
    protected class OperatorLiteral extends AbstractLeafElement {
        
        public static final String NAME = "operator-literal";
        
        private final int length;
        
        public OperatorLiteral(int startOffset, int endOffset)
                throws BadLocationException
                {
            super(MultiPartParsingElement.this, startOffset);
            int offset = startOffset;
            length = endOffset - startOffset;
            ((StyledDocument) getDocument()).setCharacterAttributes(offset, length, DEFAULT_STYLE,
                    false);
                }
        
        @Override
        public int getEndOffset()
        {
            return getStartOffset() + length;
        }
        
        @Override
        public Type getType()
        {
            return Type.UNTYPED;
        }
        
        @Override
        public String getName()
        {
            return NAME;
        }
        
        @Override
        public Element merge(Element element)
        {
            return null;
        }
        
        @Override
        public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
        {
            // nonaccessible
            return null;
        }
        
        @Override
        public List<ElementChange> postRemoveUpdate(Removal change)
        {
            // nonaccessible
            return null;
        }
        
    }
}